using System;
using System.Collections.Generic;

using TrueCode.Core.Aspects;


namespace TrueCode.DependencyManager
{
  public class ServiceCollection
  {
    private readonly Dictionary< ServiceKey, object > _services = new Dictionary< ServiceKey, object >();

    #region AddDefault
    public ServiceCollection AddDefault( [NotNull] Type typeofService, [NotNull] object service )
    {
      #region Validation
      if( typeofService == null ) throw new ArgumentNullException( "typeofService" );
      if( service == null ) throw new ArgumentNullException( "service" );

      #region Assert that service implements TService
      if( !typeofService.IsAssignableFrom( service.GetType() ) )
      {
        var exception = new ArgumentException( "Service does not implement specified type.", "service" );
        exception.Data.Add( "Service type", typeofService );
        exception.Data.Add( "Real type", service.GetType() );
        throw exception;
      }
      #endregion

      #endregion

      return Add( typeofService, null, null, service );
    }


    public ServiceCollection AddDefault< TService >( [NotNull] object service )
    {
      return AddDefault( typeof( TService ), service );
    }
    #endregion

    #region Add
    public ServiceCollection Add( [NotNull] Type typeofService, [CanBeNull] object serviceKey,
                                  [CanBeNull] SessionAim aim,
                                  [NotNull] object service )
    {
      #region Validation
      if( typeofService == null ) throw new ArgumentNullException( "typeofService" );
      if( service == null ) throw new ArgumentNullException( "service" );

      #region Assert that service implements TService
      if( !typeofService.IsAssignableFrom( service.GetType() ) )
      {
        var exception = new ArgumentException( "Service does not implement specified type.", "service" );
        exception.Data.Add( "Service type", typeofService );
        exception.Data.Add( "Real type", service.GetType() );
        throw exception;
      }
      #endregion

      #endregion

      var sk = new ServiceKey( typeofService, serviceKey, aim );
      _services.Add( sk, service );
      return this;
    }

    public ServiceCollection Add< TService >( [CanBeNull] object serviceKey, [CanBeNull] SessionAim aim,
                                              [NotNull] object service )
    {
      return Add( typeof( TService ), serviceKey, aim, service );
    }

    public ServiceCollection Add< TAim, TService >( [CanBeNull] object setting, [CanBeNull] object serviceKey,
                                                    [NotNull] object service )
    {
      if( service == null ) throw new ArgumentNullException( "service" );
      return Add( typeof( TService ), serviceKey, new SessionAim< TAim >( setting ), service );
    }
    #endregion

    #region Get
    public object Get( [NotNull] Type typeofService, [CanBeNull] object serviceKey = null,
                       [CanBeNull] SessionAim aim = null )
    {
      if( typeofService == null ) throw new ArgumentNullException( "typeofService" );

      var sk = new ServiceKey( typeofService, serviceKey, aim );
      object service;
      if( ! _services.TryGetValue( sk, out service ) && aim != null )
      {
        sk = new ServiceKey( typeofService, null, null );
        _services.TryGetValue( sk, out service );
      }
      return service;
    }

    public TService Get< TService >( [CanBeNull] object serviceKey = null, [CanBeNull] SessionAim aim = null )
    {
      return ( TService )Get( typeof( TService ), serviceKey, aim );
    }
    #endregion

    #region GetNoDefault
    public TService GetNoDefault< TService >( [CanBeNull] object serviceKey, [NotNull] SessionAim aim )
    {
      return ( TService )GetNoDefault( typeof( TService ), serviceKey, aim );
    }

    public object GetNoDefault( [NotNull] Type typeofService, [CanBeNull] object serviceKey, [NotNull] SessionAim aim )
    {
      #region Validation
      if( typeofService == null ) throw new ArgumentNullException( "typeofService" );
      if( aim == null ) throw new ArgumentNullException( "aim" );
      #endregion

      var sk = new ServiceKey( typeofService, serviceKey, aim );
      object service;
      _services.TryGetValue( sk, out service );
      return service;
    }
    #endregion

    #region Overloads

    #region Add overloads
    // Without key
    public ServiceCollection Add( [NotNull] Type typeofService, [CanBeNull] SessionAim aim, [NotNull] object service )
    {
      return Add( typeofService, null, aim, service );
    }

    // Without key
    public ServiceCollection Add< TService >( [CanBeNull] SessionAim aim, [NotNull] object service )
    {
      return Add( typeof( TService ), null, aim, service );
    }

    public ServiceCollection Add< TAim, TService >( [NotNull] object service )
    {
      return Add< TAim, TService >( null, null, service );
    }

    public ServiceCollection Add< TAim, TService >( [CanBeNull] object serviceKey, [NotNull] object service )
    {
      return Add< TAim, TService >( null, serviceKey, service );
    }
    #endregion

    #region Get overloads
    // Without key
    public object Get( [NotNull] Type typeofService, [NotNull] SessionAim aim )
    {
      if( aim == null ) throw new ArgumentNullException( "aim" );
      return Get( typeofService, null, aim );
    }

    public TService Get< TService >( [NotNull] SessionAim aim )
    {
      if( aim == null ) throw new ArgumentNullException( "aim" );
      return ( TService )Get( typeof( TService ), null, aim );
    }
    #endregion

    #region GetNoDefult
    // Without key
    public object GetNoDefault( [NotNull] Type typeofService, [NotNull] SessionAim aim )
    {
      return GetNoDefault( typeofService, null, aim );
    }

    public TService GetNoDefault< TService >( [NotNull] SessionAim aim )
    {
      if( aim == null ) throw new ArgumentNullException( "aim" );
      return ( TService )GetNoDefault( typeof( TService ), aim );
    }
    #endregion

    #endregion

    #region ServiceKey nested class
    /// <summary>
    /// Instances of this class are used as keys in the <see cref="_services"/> dictionary.
    /// </summary>
    private class ServiceKey : IEquatable< ServiceKey >
    {
      #region Fields
      [NotNull] private readonly Type _serviceType;
      [CanBeNull] private readonly SessionAim _aim;
      [CanBeNull] private readonly object _serviceKey;
      #endregion

      #region .ctors
      /// <summary>
      /// Initializes a new instance of the <see cref="ServiceKey"/> class.
      /// </summary>
      /// <param name="serviceType">Type of the service.</param>
      /// <param name="serviceKey">The type of the value stored in the dictionary.</param>
      /// <param name="aim">Extern key to associate the value with it. Can be null.</param>
      public ServiceKey( [NotNull] Type serviceType, [CanBeNull] object serviceKey, [CanBeNull] SessionAim aim )
      {
        if( serviceType == null ) throw new ArgumentNullException( "serviceType" );

        _serviceType = serviceType;
        _aim = aim;
        _serviceKey = serviceKey;
      }
      #endregion

      #region Implementation of IEquatable< ServiceKey >
      public bool Equals( ServiceKey other )
      {
        return _serviceType.Equals( other._serviceType ) && Equals( _aim, other._aim ) &&
               Equals( _serviceKey, other._serviceKey );
      }
      #endregion

      #region Implementation of object equality
      public override bool Equals( object obj )
      {
        return obj is ServiceKey ? Equals( ( ServiceKey )obj ) : false;
      }

      public override int GetHashCode()
      {
        return _serviceType.GetHashCode() ^ ( _aim == null ? 0 : _aim.GetHashCode() ) ^
               ( _serviceKey == null ? 0 : _serviceKey.GetHashCode() );
      }
      #endregion
    }
    #endregion
  }
}